/*! ******************************************************************************
 *
 * Created on 2022年3月10日
 *
 * Copyright(c) YuZhou Big Data Laboratory CO.,Ltd, 2022.  All rights reserved.
 *
 *******************************************************************************
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 ******************************************************************************/

package org.yzbdl.lanius.core.plugins.install;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Console;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.MD5;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.vfs2.FileObject;
import org.pentaho.di.core.exception.KettlePluginException;
import org.pentaho.di.core.logging.LogChannel;
import org.pentaho.di.core.logging.LogChannelInterface;
import org.pentaho.di.core.plugins.KettleURLClassLoader;
import org.pentaho.di.core.plugins.PluginFolder;
import org.pentaho.di.core.plugins.PluginRegistry;
import org.pentaho.di.core.plugins.PluginTypeInterface;
import org.scannotation.AnnotationDB;
import org.yzbdl.lanius.core.constant.LocalDir;
import org.yzbdl.lanius.authorize.verification.LicenseVerification;
import org.yzbdl.lanius.core.exception.LaniusEngineDbException;
import org.yzbdl.lanius.core.plugins.JarFileAnnotationPlugin;
import org.yzbdl.lanius.core.plugins.JarFileCache;
import org.yzbdl.lanius.core.plugins.metastore.LdrPluginDao;
import org.yzbdl.lanius.core.plugins.metastore.LdrPluginEntity;
import org.yzbdl.lanius.core.plugins.service.PluginService;

import java.io.*;
import java.lang.annotation.Annotation;
import java.net.URL;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

/**
 * @author hs@yzbdl.ac.cn
 */
public class PluginInstaller {

    protected static final LogChannelInterface LOGGER = new LogChannel("安装日志");

    protected static final String META_INFO_FILE_NAME = "meta-info.json";

    protected static final String EXT_LIB = "drivers";

    /**
     * 从压缩包导入插件
     *
     * @param zipFile
     * @throws IOException
     * @throws KettlePluginException
     * @throws LaniusEngineDbException
     */
    public static LdrPluginEntity importFromZip(File zipFile)
            throws IOException, KettlePluginException, LaniusEngineDbException {
        if (!zipFile.exists()) {
            throw new FileNotFoundException("压缩包不存在。");
        }

        File pluginFolder = PluginInstaller.unZipPluginToTmpDir(zipFile);
        try {
            return PluginInstaller.importFromFolder(pluginFolder.getAbsolutePath());
        } catch (Exception e) {
            throw e;
        } finally {
            // 删除临时文件
            FileUtil.del(pluginFolder.getParentFile().getParentFile());
        }
    }

    /**
     * 从文件夹中导入插件
     * grant_type
     *
     * @param folder
     * @throws KettlePluginException
     * @throws IOException
     * @throws LaniusEngineDbException
     */
    public static LdrPluginEntity importFromFolder(String folder)
            throws KettlePluginException, IOException, LaniusEngineDbException {
        File tempPluginFolder = new File(folder);
        if (!tempPluginFolder.exists() || !tempPluginFolder.isDirectory()) {
            throw new FileNotFoundException("目录不存在!");
        }

        List<LdrPluginEntity> pluginEntities = PluginInstaller.extractPluginInfo(folder);

        if (pluginEntities != null && pluginEntities.size() == 1) {
            LdrPluginEntity ldrPluginEntity = pluginEntities.get(0);
            if (!LicenseVerification.ACTIVE_ENTERPRISE_EDITION && pluginEntities.get(0).getEnterprise()) {
                throw new KettlePluginException("插件\"" + ldrPluginEntity.getName() + "\"只能在商业版中安装！安装源：" + folder);
            }

            File pluginFolder = new File(LocalDir.PLUGIN + LocalDir.separator + ldrPluginEntity.getPluginId()
                    + LocalDir.separator + ldrPluginEntity.getVersion());
            LdrPluginEntity entity = null;
            List<File> movedLibJars = null;
            if (pluginFolder.exists()) {
                if (ObjectUtil.isNotEmpty(LdrPluginDao.getInstance().findByPluginId(ldrPluginEntity.getPluginId()))) {
                    try {
                        // 拷贝到插件目录
                        FileUtil.copyContent(tempPluginFolder, pluginFolder, false);
                        // 合并lib依赖包
                        movedLibJars = PluginInstaller.mergePluginLibs(pluginFolder, ldrPluginEntity);
                        // 添加推送重新加载某个插件
                        entity = LdrPluginDao.getInstance().getLocalVersionOne(ldrPluginEntity.getPluginId(),
                                ldrPluginEntity.getPluginTypeName(), ldrPluginEntity.getVersion());
                        if (entity == null) {
                            // 如果数据库被删除，则重新加入数据
                            entity = PluginService.getInstance().addLocalPlugin(ldrPluginEntity);
                        }
                    } catch (Exception e) {
                        // 回滚：删除pluginFolder/移除movedLibJars/删除数据库
                        PluginInstaller.rollbackInstall(pluginFolder, movedLibJars, entity);
                        PluginInstaller.LOGGER.logError(e.getMessage(),e);
                    }
                } else {
                    try {
                        // FileUtil.del(pluginFolder);
                        // 写入数据库也要执行安装一次
                        FileUtil.copyContent(tempPluginFolder, pluginFolder, true);
                        // 合并lib依赖包
                        movedLibJars = PluginInstaller.mergePluginLibs(pluginFolder, ldrPluginEntity);

                        entity = PluginService.getInstance().addLocalPlugin(ldrPluginEntity);

                    } catch (Exception ex) {
                        PluginInstaller.rollbackInstall(pluginFolder, movedLibJars, entity);
                        PluginInstaller.LOGGER.logError(ex.getMessage(),ex);
                    }

                }
            } else {
                try {

                    // 拷贝到插件目录
                    FileUtil.copyContent(tempPluginFolder, pluginFolder, true);
                    // 合并lib依赖包
                    movedLibJars = PluginInstaller.mergePluginLibs(pluginFolder, ldrPluginEntity);
                    // 保存到数据库
                    entity = PluginService.getInstance().addLocalPlugin(ldrPluginEntity);
                } catch (Exception e) {
                    // 回滚：删除pluginFolder/移除movedLibJars/删除数据库
                    PluginInstaller.rollbackInstall(pluginFolder, movedLibJars, entity);
                    PluginInstaller.LOGGER.logError(e.getMessage(),e);
                }
            }

            try {
                entity.setInstallOutPrint(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"))+" - 安装日志 - "+entity.getName() + " - v" + entity.getVersion() + " 已安装。");
                PluginInstaller.LOGGER.logBasic(entity.getName() + " - v" + entity.getVersion() + " 已安装。");
            } catch (Exception e) {
                Console.log("获取插件信息失败:",e);
            }
            return entity;
        } else {
            throw new KettlePluginException("同一个插件包只能有一个插件:" + folder);
        }
    }

    public static File unZipPluginToTmpDir(File zipFile) throws IOException {
        String random = RandomUtil.randomString(6);
        String pluginPath = LocalDir.TEMP + random;
        File destFolder = new File(pluginPath);
        if (!destFolder.exists()) {
            FileUtil.mkdir(destFolder);
        }

        ZipInputStream zis = null;
        FileInputStream fis = null;
        BufferedInputStream bis = null;
        try {
            fis = new FileInputStream(zipFile);
            bis = new BufferedInputStream(fis);
            zis = new ZipInputStream(bis);
            ZipEntry ze = null;
            while ((ze = zis.getNextEntry()) != null) {
                // 得到解压文件在当前存储的绝对路径
                String filePath = pluginPath + File.separator + ze.getName();
                if (ze.isDirectory()) {
                    new File(filePath).mkdirs();
                } else {
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();

                    byte[] buffer = new byte[2048];
                    int count;
                    while ((count = zis.read(buffer)) != -1) {
                        baos.write(buffer, 0, count);
                    }
                    byte[] bytes = baos.toByteArray();
                    File entryFile = new File(filePath);
                    // 创建父目录
                    if (!entryFile.getParentFile().exists()) {
                        FileUtil.mkdir(entryFile.getParentFile());
                    }
                    // 写文件
                    FileOutputStream fos = new FileOutputStream(entryFile);
                    BufferedOutputStream bos = new BufferedOutputStream(fos);
                    bos.write(bytes);
                    bos.flush();

                    bos.close();
                    fos.close();
                    baos.close();
                }
            }
        } finally {
            PluginInstaller.closeQuietly(fis);
            PluginInstaller.closeQuietly(bis);
            PluginInstaller.closeQuietly(zis);
        }

        File[] child = destFolder.listFiles();
        if (child.length != 1) {
            FileUtil.del(pluginPath);
            throw new RuntimeException("压缩包文件目录结构不合法（只能是一个目录）！");
        }

        return child[0];
    }

    /**
     * 关闭输入输出流
     *
     * @param closeable 可关闭的数据源或目标
     */
    private static void closeQuietly(final Closeable closeable) {
        try {
            if (closeable != null) {
                closeable.close();
            }
        } catch (final IOException ioe) {
            Console.log("流关闭失败：",ioe);
        }
    }

    /**
     * 提取插件信息
     *
     * @return
     * @throws IOException
     * @throws ClassNotFoundException
     * @throws KettlePluginException
     */
    public static List<LdrPluginEntity> extractPluginInfo(String pluginPath) throws KettlePluginException, IOException {
        List<File> libs = PluginInstaller.findLibraries(pluginPath);
        List<LdrPluginEntity> pluginEntities = Lists.newArrayList();
        for (PluginTypeInterface pluginType : PluginRegistry.getAddedPluginTypes()) {
            Class<? extends java.lang.annotation.Annotation> at = pluginType.getPluginType();
            List<JarFileAnnotationPlugin> plugins = PluginInstaller.scanAnnotatedClassFiles(pluginPath, at.getName());
            pluginEntities.addAll(PluginInstaller.extractPluginInfo(plugins, pluginType, libs));
        }
        return pluginEntities;
    }

    /**
     * 通过扫描jar文件获取被注解的类信息
     *
     * @param pluginPath
     * @param annotationClassName
     * @return
     * @throws IOException
     * @throws KettlePluginException
     */
    private static List<JarFileAnnotationPlugin> scanAnnotatedClassFiles(String pluginPath, String annotationClassName)
            throws IOException, KettlePluginException {

        List<JarFileAnnotationPlugin> classFiles = new ArrayList<JarFileAnnotationPlugin>();
        // 遍历目录下所有文件对象，扫描其中是否包含注解
        try {
            JarFileCache jarFileCache = JarFileCache.getInstance();

            // 目录下面所有的jar文件
            FileObject[] fileObjects = jarFileCache.getFileObjects(new PluginFolder(pluginPath, false, true));

            for (FileObject fileObject : fileObjects) {
                try {
                    AnnotationDB annotationDB = jarFileCache.getAnnotationDB(fileObject);
                    // 被注解了annotationClassName的所有类文件
                    Set<String> classNames = annotationDB.getAnnotationIndex().get(annotationClassName);
                    if (classNames != null) {
                        for (String className : classNames) {
                            URL jarUrl = fileObject.getURL();
                            URL jarPathUrl = fileObject.getParent().getURL();
                            classFiles.add(new JarFileAnnotationPlugin(className, jarUrl, jarPathUrl));
                        }
                    }
                } catch (IOException e) {
                    throw new KettlePluginException(fileObject.getPublicURIString(), e);
                }
            }
        } catch (Exception e) {
            throw new KettlePluginException("扫描插件信息出错.", e);
        }
        return classFiles;
    }

    /**
     * @param pluginPath
     * @return
     */
    private static List<File> findLibraries(String pluginPath) {
        String libPath = pluginPath + LocalDir.separator + LocalDir.LIB;
        List<File> libs = FileUtil.loopFiles(libPath, f -> f.getName().toLowerCase().endsWith(".jar"));
        return libs;
    }

    /**
     * 装载插件包
     *
     * @throws KettlePluginException
     * @throws ClassNotFoundException
     * @throws IOException
     */
    private static List<LdrPluginEntity> extractPluginInfo(List<JarFileAnnotationPlugin> plugins,
                                                           PluginTypeInterface pluginType, List<File> libs) throws KettlePluginException, IOException {

        List<LdrPluginEntity> pluginEntities = Lists.newArrayList();

        if (plugins.isEmpty()) {
            return pluginEntities;
        }

        Set<URL> urls = Sets.newLinkedHashSet();
        for (File jar : libs) {
            urls.add(new File(jar.getAbsolutePath()).toURI().toURL());
        }
        for (JarFileAnnotationPlugin plugin : plugins) {
            urls.add(plugin.jarUrl());
        }

        KettleURLClassLoader classLoader =
                new KettleURLClassLoader(ArrayUtil.toArray(urls, URL.class), PluginInstaller.class.getClassLoader());

        try {
            for (JarFileAnnotationPlugin plugin : plugins) {
                Class<?> clazz = classLoader.loadClass(plugin.getClassName());
                Annotation annotation = clazz.getAnnotation(pluginType.getPluginType());
                LdrPluginEntity ldrPluginEntity = pluginType.extractAnnotationInfo(clazz, annotation);
                pluginEntities.add(ldrPluginEntity);
            }
        } catch (Exception e) {
            throw new KettlePluginException("无法加载插件信息：", e);
        } finally {
            classLoader.close();
            classLoader.closeClassLoader();
        }

        return pluginEntities;
    }

    /**
     * 合并插件依赖包
     *
     * @return
     */
    private static List<File> mergePluginLibs(File pluginDir, LdrPluginEntity entity) {
        // 获取meta-info json
        String metaInfoStr = PluginInstaller.getMetaInfoStr(pluginDir);
        entity.setMetaInfo(metaInfoStr);

        List<File> libJars = FileUtil.loopFiles(pluginDir.getAbsolutePath() + File.separator + LocalDir.LIB,
                ff -> ff.getName().toLowerCase().endsWith(".jar"));

        // 检查并排除不合并的依赖包
        if (StringUtils.isNotBlank(metaInfoStr)) {
            libJars = PluginInstaller.checkHasExcluded(libJars, metaInfoStr);
        }

        List<File> movedLibJars = Lists.newArrayList();
        List<String> md5Names = Lists.newArrayList();
        libJars.forEach(libJar -> {
            // 创建link文件
            String md5 = PluginInstaller.createLinkFileAndGetMd5(libJar);
            // 移动文件到libPlugins目录，使用md5命名，重名不覆盖
            File dest = new File(LocalDir.LIB_PLUGINS + LocalDir.separator + md5 + ".jar");
            if (!dest.exists()) {
                FileUtil.move(libJar, dest, false);
                movedLibJars.add(dest);
            } else {
                boolean del = FileUtil.del(libJar);
                // System.out.println(del);
            }
            // 收集libplugins下的文件名
            md5Names.add(md5 + ".jar");
        });

        // 填充entity属性
        entity.setLibrary(StrUtil.join(",", md5Names));

        // 返回被移动的jar包
        return movedLibJars;
    }

    /**
     * 检查是否有要排除的lib
     *
     * @param libJars
     * @param metaInfoStr
     * @return
     */
    private static List<File> checkHasExcluded(List<File> libJars, String metaInfoStr) {
        JSONObject extJson = new JSONObject(metaInfoStr);
        JSONObject extension = (JSONObject) extJson.get("extension");
        JSONArray excludeLib = null != extension ? (JSONArray) extension.get("excludeLib") : null;
        if (null != excludeLib && excludeLib.size() > 0) {
            Iterator<File> iter = libJars.iterator();
            while (iter.hasNext()) {
                File libJar = iter.next();
                for (int i = 0; i < excludeLib.size(); i++) {
                    String extLib = excludeLib.getStr(i);
                    if (libJar.getName().equals(extLib.toLowerCase())) {
                        // 移动文件到libPlugins目录，使用md5命名，重名不覆盖
                        File dest = new File(LocalDir.DRIVER_DIR + LocalDir.separator + extLib.toLowerCase());
                        if (!dest.exists()) {
                            FileUtil.move(libJar, dest, false);
                        } else {
                            FileUtil.del(libJar);
                        }
                        iter.remove();
                    }
                }
            }
            // 重新赋值是多余的操作 当前list被引用 已经移除了对象 使用下面方法存在迭代器指针位移问题，导致list直接为空
            // libJars = IteratorUtils.toList(iter);
        }
        return libJars;
    }

    /**
     * 创建jar包的link文件，并返回jar包的md5摘要
     *
     * @param file
     * @return
     */
    private static String createLinkFileAndGetMd5(File file) {
        File linkFile = new File(file.getAbsoluteFile() + ".link");
        String md5 = MD5.create().digestHex(file);
        FileUtil.writeString(md5, linkFile, "UTF-8");
        return md5;
    }

    /**
     * 创建jar包的link文件，并返回jar包的md5摘要
     *
     * @param file
     * @return
     */
    private static String getMetaInfoStr(File file) {
        File metaInfo = new File(file.getAbsoluteFile() + LocalDir.separator + PluginInstaller.META_INFO_FILE_NAME);
        if (!metaInfo.exists()) {
            return null;
        }
        return FileUtil.readString(metaInfo, "UTF-8");
    }

    /**
     * 回滚插件的安装
     */
    private static void rollbackInstall(File pluginFolder, List<File> movedLibJars, LdrPluginEntity entity) {
        try {
            PluginService.getInstance().delete(entity);
            FileUtil.del(pluginFolder);
            for (File jar : movedLibJars) {
                FileUtil.del(jar);
            }
        } catch (Exception e) {
            throw new RuntimeException("插件安装失败, 回滚出错：", e);
        }
    }

    /**
     * @简介
     * @作者 zh
     * @时间 2021/3/25 15:08
     * @参数 [pluginIds] 需要查找的插件Ids [File] 需要查找插件的文件夹
     * @返回值 java.util.List<org.pentaho.di.core.plugins.metastore.PluginEntity>
     */
    public static List<File> findPluginJarById(File File, List<String> pluginIds) throws IOException {
        List<java.io.File> files = Arrays.asList(File.listFiles());
        ArrayList<File> returnFile = new ArrayList<>();
        for (java.io.File file : files) {
            File pluginFolder = PluginInstaller.unZipPluginToTmpDir(file);
            try {
                List<LdrPluginEntity> pluginEntities = PluginInstaller.extractPluginInfo(pluginFolder.getAbsolutePath());
                if (pluginIds.indexOf(pluginEntities.get(0).getPluginId()) >= 0) {
                    returnFile.add(file);
                }
            } catch (KettlePluginException e) {
                LOGGER.logError(e.getMessage(),e);
            } finally {
                // 删除临时文件
                FileUtil.del(pluginFolder.getParentFile());
            }
        }
        return returnFile;
    }
}